---
title: "Monitoring latency: Cloudflare Workers vs Fly vs Koyeb vs Railway vs Render"
description: ""
author: "Thibault Le Ouay Ducasse"
publishedAt: "2024-02-19"
image: "/assets/posts/monitoring-latency/all-hosting-providers.png"
category: "education"
---

> ⚠️ We are using the default settings for each provider and conducting
> datacenter to datacenter requests. A real-world application's results are
> going to be different. ⚠️

You want to know which cloud providers offer the lowest latency?

In this post, I compare the latency of
[Cloudflare Workers](#cloudflare-workers), [Fly](#flyio), [Koyeb](#koyeb),
[Railway](#railway) and [Render](#render) using
[OpenStatus](https://www.openstatus.dev).

I deployed the application on the cheapest or free tier offered by each
provider.

For this test, I used a basic [Hono](https://hono.dev) server that returns a
simple text response.

```js
const app = new Hono();
app.use("*", logger());

app.use("*", poweredBy());

app.get("/", (c) => {
  return c.text(
    "Just return the desired http status code, e.g. /404 🤯 \nhttps://www.openstatus.dev",
  );
});
```

You can find the code [here](https://github.com/openstatusHQ/status-code), it’s
open source 😉.

OpenStatus monitored our endpoint every **10 minutes** from **6 locations**
located in Amsterdam, Ashburn, Hong Kong, Johannesburg, Sao Paulo and Sydney.

It's a good way to test our own product and improve it.

Let's analyze the data from the past two weeks.

## Cloudflare workers

Cloudflare Workers is a serverless platform by Cloudflare. It lets you build new
applications using JavaScript/Typescript. You can deploy up to 100 worker
scripts for free, running on more than 275 network locations.

### Latency metrics

<Grid cols={5}>
<div>

**100**% UPTIME

</div>
<div>

**0**# FAILS

</div>
<div>

**10,956**# PINGS

</div>
<div className="hidden md:block"></div>
<div className="hidden md:block"></div>
<div>

**182**ms AVG

</div>
<div>

**138**ms P75

</div>
<div>

**695**ms P90

</div>
<div>

**778**ms P95

</div>
<div>

**991**ms P99

</div>
</Grid>

<div className="mt-4">
  <SimpleChart
    staticFile="/assets/posts/monitoring-latency/cloudflare.json"
    caption="Cloudflare avg. latency between 04. Feb and 18. Feb 2024 aggregated in a 1h window."
  />
</div>

### Timing metrics

<Table
  data={{
    headers: ["Region", "DNS (ms)", "Connection (ms)", "TLS Handshake (ms)", "TTFB (ms)", "Transfert (ms)"],
    rows: [
      ["AMS", "17", "2", "17", "27", "0"],
      ["GRU", "38", "2", "13", "28", "0"],
      ["HKG", "19", "2", "13", "29", "0"],
      ["IAD", "24", "1", "14", "30", "0"],
      ["JNB", "123", "168", "182", "185", "0"],
      ["SYD", "51", "1", "11", "25", "0"],
    ],
  }}
/>

I can notice that Johannesburg's latency is about ten times higher than that of
the other monitors.

### Headers

From the Cloudflare request I can get the location of the workers that handle
the request, with `Cf-ray` in the headers response.

<Table
  data={{
    headers: ["Checker region", "Workers region", "number of request"],
    rows: [
      ["HKG", "HKG", "1831"],
      ["SYD", "SYD", "1831"],
      ["AMS", "AMS", "1831"],
      ["IAD", "IAD", "1831"],
      ["GRU", "GRU", "1791"],
      ["GRU", "GIG", "40"],
      ["JNB", "AMS", "741"],
      ["JNB", "MUC", "4"],
      ["JNB", "HKG", "5"],
      ["JNB", "SIN", "6"],
      ["JNB", "NRT", "8"],
      ["JNB", "EWR", "10"],
      ["JNB", "CDG", "82"],
      ["JNB", "FRA", "276"],
      ["JNB", "LHR", "699"],
      ["JNB", "AMS", "741"],
    ],
  }}
/>

I can see all the request from JNB is never routed to a nearby data-center.

Apart from the strange routing error in Johannesburg, Cloudflare workers are
fast worldwide.

I have not experienced any cold start issues.

## Fly.io

Fly.io simplifies deploying and running server-side applications globally.
Developers can deploy their applications near users worldwide for low latency
and high performance. It uses a lightweight Firecracker VM to easily deploy
Docker images.

### Latency metrics

<Grid cols={5}>
<div>

**100**% UPTIME

</div>
<div>

**0**# FAILS

</div>
<div>

**10,952**# PINGS

</div>
<div className="hidden md:block"></div>
<div className="hidden md:block"></div>
<div>

**1,471**ms AVG

</div>
<div>

**1,514**ms P75

</div>
<div>

**1,555**ms P90

</div>
<div>

**1,626**ms P95

</div>
<div>

**2,547**ms P99

</div>
</Grid>

<div className="mt-4">
  <SimpleChart
    staticFile="/assets/posts/monitoring-latency/fly.json"
    caption="Fly avg. latency between 04. Feb and 18. Feb 2024 aggregated in a 1h window."
  />
</div>

### Timing metrics

<Table
  data={{
    headers: ["Region", "DNS (ms)", "Connection (ms)", "TLS Handshake (ms)", "TTFB (ms)", "Transfert (ms)"],
    rows: [
      ["AMS", "6", "1", "8", "1469", "0"],
      ["GRU", "5", "0", "4", "1431", "0"],
      ["HKG", "4", "0", "5", "1473", "0"],
      ["IAD", "3", "0", "5", "1470", "0"],
      ["JNB", "24", "0", "5", "1423", "0"],
      ["SYD", "3", "0", "3", "1489", "0"],
    ],
  }}
/>

The DNS is fast, our checker is attempting to connect to a region in the same
data center, but our machine's cold start is slowing us down, leading to the
high TTFB.

Here’s our config for Fly.io:

```toml
app = 'statuscode'
primary_region = 'ams'

[build]
  dockerfile = "./Dockerfile"

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0
  processes = ['app']

[[vm]]
  cpu_kind = 'shared'
  cpus = 1
  memory_mb = 256
```

The primary region of our server is Amsterdam, and the fly instances is getting
paused after a period of inactivity.

The machine starts slowly, as indicated by the logs showing a start time of
`1.513643778s.`

```
2024-02-14T11:24:16.107 proxy[286560ea703108] ams [info] Starting machine

2024-02-14T11:24:16.322 app[286560ea703108] ams [info] [ 0.035736] PCI: Fatal: No config space access function found

2024-02-14T11:24:16.533 app[286560ea703108] ams [info] INFO Starting init (commit: bfa79be)...

2024-02-14T11:24:16.546 app[286560ea703108] ams [info] INFO Preparing to run: `/usr/local/bin/docker-entrypoint.sh bun start` as root

2024-02-14T11:24:16.558 app[286560ea703108] ams [info] INFO [fly api proxy] listening at /.fly/api

2024-02-14T11:24:16.565 app[286560ea703108] ams [info] 2024/02/14 11:24:16 listening on [fdaa:3:2ef:a7b:10c:3c9a:5b4:2]:22 (DNS: [fdaa::3]:53)

2024-02-14T11:24:16.611 app[286560ea703108] ams [info] $ bun src/index.ts

2024-02-14T11:24:16.618 runner[286560ea703108] ams [info] Machine started in 460ms

2024-02-14T11:24:17.621 proxy[286560ea703108] ams [info] machine started in 1.513643778s

2024-02-14T11:24:17.628 proxy[286560ea703108] ams [info] machine became reachable in 7.03669ms
```

#### OpenStatus Prod metrics

If you update your fly.toml file to include the following, you can get the zero
cold start and achieve a better latency.

```
  min_machines_running = 1
```

This is our data for our production server deploy on Fly.io.

<Grid cols={5}>
<div>

**100**% UPTIME

</div>
<div>

**0**# FAILS

</div>
<div>

**12,076**# PINGS

</div>
<div className="hidden md:block"></div>
<div className="hidden md:block"></div>
<div>

**61**ms AVG

</div>
<div>

**67**ms P75

</div>
<div>

**164**ms P90

</div>
<div>

**198**ms P95

</div>
<div>

**327**ms P99

</div>
</Grid>

> We use Fly.io in production, and the machine never sleeps, yielding much
> better results.

## Koyeb

Koyeb is a developer-friendly serverless platform that allows for global app
deployment without the need for operations, servers, or infrastructure
management. Koyeb offers a free Starter plan that includes one Web Service, one
Database service. The platform focuses on ease of deployment and scalability for
developers

### Latency metrics

<Grid cols={5}>
<div>

**100**% UPTIME

</div>
<div>

**0**# FAILS

</div>
<div>

**10,955**# PINGS

</div>
<div className="hidden md:block"></div>
<div className="hidden md:block"></div>
<div>

**539**ms AVG

</div>
<div>

**738**ms P75

</div>
<div>

**881**ms P90

</div>
<div>

**1,013**ms P95

</div>
<div>

**1,525**ms P99

</div>
</Grid>

<div className="mt-4">
  <SimpleChart
    staticFile="/assets/posts/monitoring-latency/koyeb.json"
    caption="Koyeb avg. latency between 04. Feb and 18. Feb 2024."
  />
</div>

### Timing metrics

<Table
  data={{
    headers: ["Region", "DNS (ms)", "Connection (ms)", "TLS Handshake (ms)", "TTFB (ms)", "Transfert (ms)"],
    rows: [
      ["AMS", "50", "2", "17", "107", "0"],
      ["GRU", "139", "65", "75", "407", "0"],
      ["HKG", "48", "2", "13", "321", "0"],
      ["IAD", "35", "1", "12", "129", "0"],
      ["JNB", "298", "1", "11", "720", "0"],
      ["SYD", "97", "1", "10", "711", "0"],
    ],
  }}
/>

### Headers

The request headers show that none of our requests are cached. They contain
`cf-cache-status: dynamic`. Cloudflare handles the Koyeb edge layer.
https://www.koyeb.com/blog/building-a-multi-region-service-mesh-with-kuma-envoy-anycast-bgp-and-mtls

Our requests follow this route:

```
Cf workers -> koyeb Global load balancer -> koyeb backend
```

Let's see where did we hit the cf workers

<Table
  data={{
    headers: ["Checker region", "Workers region", "number of request"],
    rows: [
      ["AMS", "AMS", "1866"],
      ["GRU", "GRU", "504"],
      ["GRU", "IAD", "38"],
      ["GRU", "MIA", "688"],
      ["GRU", "EWR", "337"],
      ["GRU", "CIG", "299"],
      ["HKG", "HKG", "1866"],
      ["IAD", "IAD", "1866"],
      ["JNB", "JNB", "1861"],
      ["JNB", "AMS", "1"],
      ["SYD", "SYD", "1866"],
    ],
  }}
/>

Koyeb Global Load Balancer region we hit:

<Table
  data={{
    headers: ["Checker region", "Koyeb Global Load Balancer", "number of request"],
    rows: [
      ["AMS", "FRA1", "1866"],
      ["GRU", "WAS1", "1866"],
      ["HKG", "SIN1", "1866"],
      ["IAD", "WAS1", "1866"],
      ["JNB", "PAR1", "4"],
      ["JNB", "SIN1", "1864"],
      ["JNB", "FRA1", "1"],
      ["JNB", "SIN1", "1866"],
    ],
  }}
/>

I have deployed our app in the Frankfurt data-center.

## Railway

Railway is a cloud platform designed for building, shipping, and monitoring
applications without the need for Platform Engineers. It simplifies the
application development process by offering seamless deployment and monitoring
capabilities.

### Latency metrics

<Grid cols={5}>
<div>

**99.991**% UPTIME

</div>
<div>

**1**# FAILS

</div>
<div>

**10,955**# PINGS

</div>
<div className="hidden md:block"></div>
<div className="hidden md:block"></div>
<div>

**381**ms AVG

</div>
<div>

**469**ms P75

</div>
<div>

**653**ms P90

</div>
<div>

**661**ms P95

</div>
<div>

**850**ms P99

</div>
</Grid>

<div className="mt-4">
  <SimpleChart
    staticFile="/assets/posts/monitoring-latency/railway.json"
    caption="Railway avg. latency between 04. Feb and 18. Feb 2024 aggregated in a 1h window."
  />
</div>

### Timing metrics

<Table
  data={{
    headers: ["Region", "DNS (ms)", "Connection (ms)", "TLS Handshake (ms)", "TTFB (ms)", "Transfert (ms)"],
    rows: [
      ["AMS", "9", "21", "18", "158", "0"],
      ["GRU", "14", "115", "127", "178", "0"],
      ["HKG", "8", "45", "54", "225", "0"],
      ["IAD", "7", "2", "14", "65", "0"],
      ["JNB", "18", "193", "178", "319", "0"],
      ["SYD", "21", "108", "105", "280", "0"],
    ],
  }}
/>

### Headers

The headers don't provide any information.

Railway is using Google Cloud Platform. It’s the only service that does not
allow us to pick a specific region on the free plan. Our test app will be
located to `us-west1` Portland, Oregon. We can see that the latency is the
lowest in IAD.

By default our app did not scale down to 0. It was always running. We don't have
any cold start.

## Render

Render is a platform that simplifies deploying and scaling web applications and
services. It offers features like automated SSL, automatic scaling, native
support for popular frameworks, and one-click deployments from Git. The platform
focuses on simplicity and developer productivity.

### Latency metrics

<Grid cols={5}>
<div>

**99.89**% UPTIME

</div>
<div>

**12**# FAILS

</div>
<div>

**10,946**# PINGS

</div>
<div className="hidden md:block"></div>
<div className="hidden md:block"></div>
<div>

**451**ms AVG

</div>
<div>

**447**ms P75

</div>
<div>

**591**ms P90

</div>
<div>

**707**ms P95

</div>
<div>

**902**ms P99

</div>
</Grid>

<div className="mt-4">
  <SimpleChart
    staticFile="/assets/posts/monitoring-latency/render.json"
    caption="Render avg. latency between 04. Feb and 18. Feb 2024 aggregated in a 1h window."
  />
</div>

### Timing metrics

<Table
  data={{
    headers: ["Region", "DNS (ms)", "Connection (ms)", "TLS Handshake (ms)", "TTFB (ms)", "Transfert (ms)"],
    rows: [
      ["AMS", "20", "2", "7", "107", "0"],
      ["GRU", "61", "2", "6", "407", "0"],
      ["HKG", "76", "2", "6", "321", "0"],
      ["IAD", "15", "1", "5", "129", "0"],
      ["JNB", "36", "161", "167", "720", "0"],
      ["SYD", "103", "1", "4", "711", "0"],
    ],
  }}
/>

### Headers

The headers don't provide any information.

I have deployed our app in the Frankfurt data-center.

According to the Render docs, the free tier will shut down the service after 15
minutes of inactivity. However, our app is being accessed by a monitor every 10
minutes. We should never scale down to 0.

```
Render spins down a Free web service that goes 15 minutes without receiving inbound traffic. Render spins the service back up whenever it next receives a request to process.
```

I think the failures are due to the cold start of our app. We have a default
timeout of 30s and the render app takes up to 50s to start.We might have hit an
inflection point between cold and warm.

## Conclusion

Here are the results of our test:

<Table
  data={{
    headers: ["Provider", "Uptime", "Fails Ping", "Total Pings", "AVG latency (ms)", "P75 (ms)", "P90 (ms)", "P95 (ms)", "P99 (ms)"],
    rows: [
      ["CF Workers", "100", "0", "10,956", "182", "138", "690", "778", "991"],
      ["Fly.io", "100", "0", "10,952", "1,471", "1,514", "1,555", "1,626", "2,547"],
      ["Koyeb", "100", "0", "10,955", "536", "738", "881", "1,013", "1,525"],
      ["Railway", "99.991", "1", "10,955", "381", "469", "653", "661", "850"],
      ["Render", "99.89", "12", "10,946", "451", "447", "591", "707", "902"],
    ],
  }}
/>

If you value low latency, Cloudflare Workers are the best option for fast global
performance without cold start issues. They deploy your app worldwide
efficiently.

For multi-region deployment, check out Koyeb and Fly.io.

For specific region deployment, Railway and Render are good choices.

Choosing a cloud provider involves considering not just latency but also user
experience and pricing.

We use Fly.io in production and are satisfied with it.

#### Vercel

I haven't included Vercel in this test. But we have a blog post comparing Vercel
Serverless vs Edge vs Serverless. You can find it
[here](/blog/monitoring-latency-vercel-edge-vs-serverless).

If you want to monitor your API or website, create an account on
[OpenStatus](/app/sign-up?ref=blog-monitoring).
